В вашем распоряжении данные сервиса Яндекс.Недвижимость — архив объявлений о продаже квартир в Санкт-Петербурге и соседних населённых пунктов за несколько лет. Задача — установить параметры, влияющие на определение рыночной стоимости. Это позволит построить автоматизированную систему: она отследит аномалии и мошенническую деятельность.
По каждой квартире на продажу доступны два вида данных. Первые вписаны пользователем, вторые — получены автоматически на основе картографических данных. Например, расстояние до центра, аэропорта, ближайшего парка и водоёма.
В настоящем исследовании предстоит изучить, обработать представленные данные. Провести исследование и сделать выводы по поставленным вопросам.
В датасете в табличном формате CSV присутствуют следующие данные по квартирам, которые продавались в Санкт-Петербурге и Лениниградской области в 2014 - 2019 годах:
Описание данных
airports_nearest — расстояние до ближайшего аэропорта в метрах (м)balcony — число балконов (шт)ceiling_height — высота потолков (м)cityCenters_nearest — расстояние до центра города (м)days_exposition — сколько дней было размещено объявление (от публикации до снятия)first_day_exposition — дата публикацииfloor — этаж (номер)floors_total — всего этажей в доме (шт)is_apartment — апартаменты (булев тип)kitchen_area — площадь кухни в квадратных метрах (м²)last_price — цена на момент снятия с публикации (руб)living_area — жилая площадь в квадратных метрах (м²)locality_name — название населённого пунктаopen_plan — свободная планировка (булев тип)parks_around3000 — число парков в радиусе 3 км (шт)parks_nearest — расстояние до ближайшего парка (м)ponds_around3000 — число водоёмов в радиусе 3 км (шт)ponds_nearest — расстояние до ближайшего водоёма (м)rooms — число комнат (шт)studio — квартира-студия (булев тип)total_area — общая площадь квартиры в квадратных метрах (м²)total_images — число фотографий квартиры в объявлении (шт)Изучить следующие параметры объектов:
Построить отдельные гистограммы для каждого из этих параметров.
Описать наблюдения по параметрам в ячейке с типом markdown.
days_exposition).¶markdown описать, сколько времени обычно занимает продажа. Какие продажи можно считать быстрыми, а какие — необычно долгими?Изучить, зависит ли цена от:
Построить графики, которые покажут зависимость цены от указанных выше параметров.
locality_name.¶locality_name и вычислить среднюю цену каждого километра. Описать, как стоимость объектов зависит от расстояния до центра города.¶Подключение необходимых библиотек осуществляю с применением инструкции Python import указывая ключевое слово import и имя нужной мне библиотеки. Также использую ключевое слово as для присвоения короткого псевдонима
# import libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Данные в формате csv загружаю с использованием функции read_csv() библиотеки pandas, в которую передаю путь к данным. Результат выполнения функции помещаю в переменную data
data = pd.read_csv('real_estate_data.csv', sep='\t')
Вывожу на экран общую информацию о файле
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 23699 entries, 0 to 23698 Data columns (total 22 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 total_images 23699 non-null int64 1 last_price 23699 non-null float64 2 total_area 23699 non-null float64 3 first_day_exposition 23699 non-null object 4 rooms 23699 non-null int64 5 ceiling_height 14504 non-null float64 6 floors_total 23613 non-null float64 7 living_area 21796 non-null float64 8 floor 23699 non-null int64 9 is_apartment 2775 non-null object 10 studio 23699 non-null bool 11 open_plan 23699 non-null bool 12 kitchen_area 21421 non-null float64 13 balcony 12180 non-null float64 14 locality_name 23650 non-null object 15 airports_nearest 18157 non-null float64 16 cityCenters_nearest 18180 non-null float64 17 parks_around3000 18181 non-null float64 18 parks_nearest 8079 non-null float64 19 ponds_around3000 18181 non-null float64 20 ponds_nearest 9110 non-null float64 21 days_exposition 20518 non-null float64 dtypes: bool(2), float64(14), int64(3), object(3) memory usage: 3.7+ MB
Загруженная таблица состоит из 22 колонок и 23699 строк. В колонках данные различных типов. Детальнее каждый столбец изучу в процессе исследования. Сейчас могу отметить, что есть пропуски в данных, их много и необходимо с этим разобраться. Присутствуют как непрерывные данные, так и категориальные. Есть данные, которые требуют изменения типа - first_day_exposition (дата публикации объявления) имеет тип данных object, необходимо преобразовать в тип date. Также имеет смысл в столбцах, где данные явно челочисленные, изменить тип данных с float64 на int64, что позволит исользовать их как категориальные при необходимости (количество этажей, балконов, парков и водоемов поблизости).
Смотрю в первом приближении распределение значений в стобцах с помощью частотной гистограммы
data.hist(figsize=(15, 20), edgecolor='black');
В столбцах с непрерывными данными вижу явные аномалии - сильное смещение графиков влево. Это значит, что присутствуют в небольшом количестве данные с очень высокими значениями. Изучу их в процессе предобработки и исследования. Также по графикам видно, что по количеству значений в столбцах лидируют объекты в пятиэтажных зданиях. Любопытно, проверю их долю от общего количества
amount_five_floor = len(data.query('floors_total == 5'))
display(f'Доля объектов в пятиэтажных зданиях - \
{round(amount_five_floor/ len(data) * 100, 2)} процента от общего количества')
'Доля объектов в пятиэтажных зданиях - 24.42 процента от общего количества'
Вывожу на экран первые пять строк загруженной таблицы
display(data.head(5))
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | kitchen_area | balcony | locality_name | airports_nearest | cityCenters_nearest | parks_around3000 | parks_nearest | ponds_around3000 | ponds_nearest | days_exposition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20 | 13000000.0 | 108.0 | 2019-03-07T00:00:00 | 3 | 2.70 | 16.0 | 51.0 | 8 | NaN | ... | 25.0 | NaN | Санкт-Петербург | 18863.0 | 16028.0 | 1.0 | 482.0 | 2.0 | 755.0 | NaN |
| 1 | 7 | 3350000.0 | 40.4 | 2018-12-04T00:00:00 | 1 | NaN | 11.0 | 18.6 | 1 | NaN | ... | 11.0 | 2.0 | посёлок Шушары | 12817.0 | 18603.0 | 0.0 | NaN | 0.0 | NaN | 81.0 |
| 2 | 10 | 5196000.0 | 56.0 | 2015-08-20T00:00:00 | 2 | NaN | 5.0 | 34.3 | 4 | NaN | ... | 8.3 | 0.0 | Санкт-Петербург | 21741.0 | 13933.0 | 1.0 | 90.0 | 2.0 | 574.0 | 558.0 |
| 3 | 0 | 64900000.0 | 159.0 | 2015-07-24T00:00:00 | 3 | NaN | 14.0 | NaN | 9 | NaN | ... | NaN | 0.0 | Санкт-Петербург | 28098.0 | 6800.0 | 2.0 | 84.0 | 3.0 | 234.0 | 424.0 |
| 4 | 2 | 10000000.0 | 100.0 | 2018-06-19T00:00:00 | 2 | 3.03 | 14.0 | 32.0 | 13 | NaN | ... | 41.0 | NaN | Санкт-Петербург | 31856.0 | 8098.0 | 2.0 | 112.0 | 1.0 | 48.0 | 121.0 |
5 rows × 22 columns
В столбцах есть назаполненные данные. Об этом говорят значения NaN (Not a Value)
При первичном изучении видно, что в представленном датасете есть пропуски (NaN) в данных. Есть странно высокие значения по столбцам с площадями квартир, жилой площади и кухни, а также аномально выглядят количество этажей в зданиях более 40, количество комнат 10 и более, высота потолков ниже 2м и выше 5м. Также имеет смысл исправить типы данных: в столбцах, где данные содержат количество неделимых объектов этажей, балконов, парков, водоемов, дней экспозиции лучше использовать целые числа. Столбец, содержащий информацию апартаменты или жилое помещение, предпочтительнее булевый тип данных, столбец с датой публикации соответственно должен иметь тип данных datetime
Смотрю количество пропусков по столбцам с помощью функций isna() и sum()
data.isna().sum()
total_images 0 last_price 0 total_area 0 first_day_exposition 0 rooms 0 ceiling_height 9195 floors_total 86 living_area 1903 floor 0 is_apartment 20924 studio 0 open_plan 0 kitchen_area 2278 balcony 11519 locality_name 49 airports_nearest 5542 cityCenters_nearest 5519 parks_around3000 5518 parks_nearest 15620 ponds_around3000 5518 ponds_nearest 14589 days_exposition 3181 dtype: int64
'locality_name' - Место расположения¶Ввиду того, что для объектов недвижимости первым по значимости параметром является место расположения, то оставлять в столбце 'locality_name' пропуски не целесообразно. Можно заполнить их пустой строкой, но полагаю возможным строки, в которых не указан адрес, из исследования исключить. Их всего 49 из 23699, поэтому данное действие не окажет существенного влияния на результат исследования:
data = data.dropna(subset=['locality_name'])
'floors_total' - Количество этажей¶Аналогично из исследования можно исключить строки c пропусками в столбце 'floors_total'. Можно попробовать заполнить их наиболее часто встречающимися значениями, опираясь на номер этажа из столбца 'floor'. Однако, учитывая наличие всего 86 пропусков из 23699, отуствие этих строк не повлияет на результат:
data = data.dropna(subset=['floors_total'])
'ceiling_height' - Высота потолков¶Смотрю столбец с данными о высоте потолков
data.ceiling_height.describe()
count 14481.000000 mean 2.771283 std 1.261983 min 1.000000 25% 2.510000 50% 2.650000 75% 2.800000 max 100.000000 Name: ceiling_height, dtype: float64
Вижу, что 50% значений находится в диапазоне от 2.5 м до 2.8 м. Среднее значение будет 2.77. Однако, предварительно выявлено, что почти четверть всех зданий это пятиэтажки, большая часть которых были построены в 50-70-е годы. Это типовые серии, в которых высота потолков была в диапазоне 2.5 - 2.65 м. Поэтому пропуски целесообразнее заполнить медианным значением 2.65м:
data['ceiling_height'] = data['ceiling_height'].fillna(data['ceiling_height'].median())
'living_area' - жилая площадь и 'kitchen_area' - площадь кухни¶Данные о жилой площади зависят от планировки и количества комнат. Поэтому логично найти медианное значение жилой площади в каждой группе квартир и заполнить им соответствующие пропуски. Такой же подход можно использовать и при заполнении пропусков значений площади кухни. При заполнении добавляю проверку, чтобы сумма площади кухни и жилой площади не превышала общую площадь квартиры:
# filling gaps in column `living_area` with median
for r in data['rooms'].unique():
data.loc[(data['rooms'] == r) & (data['living_area'].isna()), 'living_area'] = \
data.groupby('rooms')['living_area'].median()[r]
# filling gaps in column `kitchen_area` with median
for r in data['rooms'].unique():
data.loc[(data['rooms'] == r) & (data['kitchen_area'].isna()), 'kitchen_area'] =\
data.groupby('rooms')['kitchen_area'].median()[r]
data.loc[(data['rooms'] == 0) & (data['kitchen_area'].isna()),
'kitchen_area'] = data.fillna(0)
Задаю фукцию для проверки превышения общей площади. Если такое превышение выявлено, задаю понижающий коэффицинет к площадям комнаты и кухни:
# check total area
def check_area(x):
"""
Сhecks the values according to the condition and adjusts them
parameters:
x - dataset row for checking;
returns:
dataset row with modified values by condition
"""
k = x.loc['kitchen_area']
l = x.loc['living_area']
t = x.loc['total_area']
if (k + l) > t:
k = 0.8 * k
l = 0.8 * l
x.loc['kitchen_area'] = k
x.loc['living_area'] = l
return x
Применяю функцию к датасету
data = data.apply(check_area, axis=1)
'is_apartment'¶В столбце, который показывает, является ли объект аппартаментами, вероятно пропуски говорят о том, что это жилые квартиры. Исходя из этого, отсуствющие значения заполняю False
data['is_apartment'] = data['is_apartment'].fillna(value=False)
'balcony'¶В столбце, где указано количество балконов, пропуск значений с высокой долей вероятности означает отсутствие балконов, поэтому заполняю пропуски нулями - 0
data['balcony'] = data['balcony'].fillna(0)
'days_exposition' количество дней размещения объявления с момента публикации до снятия¶Возможная причина пропусков данных в столбце с периодом экспозиции состоит в том, что если объявление еще не снято, то период на высчитан, соответственно значения в этом столбце нет. Оставлю как есть. В дальнейшем, если в ходе исследования данные пропуски будут критичны, приму меры к их устранению
'airports_nearest', 'cityCenters_nearest', 'ponds_nearest', 'parks_nearest'¶Смотрю столбцы, которые связаны с картографическими данными: расстояния до парков, водоемов, центра города и аэропорта.
data[['airports_nearest', 'cityCenters_nearest', 'ponds_nearest', 'parks_nearest']].head(10)
| airports_nearest | cityCenters_nearest | ponds_nearest | parks_nearest | |
|---|---|---|---|---|
| 0 | 18863.0 | 16028.0 | 755.0 | 482.0 |
| 1 | 12817.0 | 18603.0 | NaN | NaN |
| 2 | 21741.0 | 13933.0 | 574.0 | 90.0 |
| 3 | 28098.0 | 6800.0 | 234.0 | 84.0 |
| 4 | 31856.0 | 8098.0 | 48.0 | 112.0 |
| 5 | NaN | NaN | NaN | NaN |
| 6 | 52996.0 | 19143.0 | NaN | NaN |
| 7 | 23982.0 | 11634.0 | NaN | NaN |
| 8 | NaN | NaN | NaN | NaN |
| 9 | 50898.0 | 15008.0 | NaN | NaN |
По картографическим данным можно заметить, что в данных о расстояниях значения отсуствуют по одному и тому же объекту в нескольких колонках. Возможно причина этого, отсутствие каких-либо настроек при автоматическом заполнении картографических данных, связанных с определением места расположения объекта.
Учитывая тот факт, что в датасете из адреса объекта известно только наименование населенного пункта без указания улицы и номера дома, точное определение расстояний невозможно. Заполнение усредненными значениями или медианными также очень далеко от реальности, так как размеры города Санкт - Петербурга таковы, что расстояния до аэропорта могут отличаться в несколько раз у объектов из одного города. Оставляю пока эти данные как есть. Если в процессе исследования будут вопросы, затрагивающие эти данные, буду принимать решение, как избавляться от этих пропусков
'parks_around3000', 'parks_around3000'¶В отношении заполнения пропусков значений в столбцах количество парков и водоемов в близости от объекта также не представляется возможным без знания точного адреса. Пока оставлю как есть. В исследовательском анализе приму решение, если необходимо будет ликвидировать пропуски для достижения целей исследования
Проверяю наличие явных дубликатов строк
data.duplicated().sum()
0
Явных дубликатов нет
Для корректного проведения различных операций со столбцами, в том числе математических приведу типы данных в столбцах к более подходящим типам. Например, столбцы с данными, в которых есть целые числа и дробные числа, должны иметь соответстующий тип данных 'int' и 'float'. Столбцы со значениями True и False должны иметь тип данных 'bool'. Столбцы, в которых содержатся текстовые данные могут иметь тип 'object', а содержащие даты - 'datetime'. Это позволит использовать соответствующие функции Python для работы с этими дынными целиком по столбцам. После приведения к нужным типам датасет будет готов к дальнейшей работе.
for i in ['floors_total', 'balcony', 'parks_around3000', 'ponds_around3000', 'days_exposition']:
data[i] = np.floor(pd.to_numeric(data[i], errors='coerce')).astype('Int64')
data['first_day_exposition'] = pd.to_datetime(data['first_day_exposition'])
data.info()
<class 'pandas.core.frame.DataFrame'> Index: 23565 entries, 0 to 23698 Data columns (total 22 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 total_images 23565 non-null int64 1 last_price 23565 non-null float64 2 total_area 23565 non-null float64 3 first_day_exposition 23565 non-null datetime64[ns] 4 rooms 23565 non-null int64 5 ceiling_height 23565 non-null float64 6 floors_total 23565 non-null Int64 7 living_area 23565 non-null float64 8 floor 23565 non-null int64 9 is_apartment 23565 non-null bool 10 studio 23565 non-null bool 11 open_plan 23565 non-null bool 12 kitchen_area 23565 non-null float64 13 balcony 23565 non-null Int64 14 locality_name 23565 non-null object 15 airports_nearest 18041 non-null float64 16 cityCenters_nearest 18064 non-null float64 17 parks_around3000 18065 non-null Int64 18 parks_nearest 8030 non-null float64 19 ponds_around3000 18065 non-null Int64 20 ponds_nearest 9036 non-null float64 21 days_exposition 20394 non-null Int64 dtypes: Int64(5), bool(3), datetime64[ns](1), float64(9), int64(3), object(1) memory usage: 4.3+ MB
На предыдущем этапе графики гистограммы - частоты распределения значений, показали, что есть необычно высокие значения в отдельных столбцах. Для поиска аномалий, изучу каждый из столбцов 'last_price', 'total_area', 'ceiling_height', 'floors_total' отдельно
'floors_total' - количество этажей¶Смотрю уникальные значения в столбце
data['floors_total'].sort_values().unique()
<IntegerArray> [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 33, 34, 35, 36, 37, 52, 60] Length: 36, dtype: Int64
Согласно ТОП-10 самых высоких зданий Санкт-Петербурге, здесь нет жилых зданий высотой более 37 этажей. В нашем случае это две квартиры, одна из которых находится в Кронштадте на 4-м этаже и видимо опечатка, не 60, а 6 этажей. Вторая на 18-м этаже, 52-х этажного здания. Здесь также возможна опечатка. Возможно имели ввиду 22 или 32 этажа. Вероятность, что 22 выше, оставляю это значение.
data[data['floors_total'] > 37]
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | kitchen_area | balcony | locality_name | airports_nearest | cityCenters_nearest | parks_around3000 | parks_nearest | ponds_around3000 | ponds_nearest | days_exposition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2253 | 12 | 3800000.0 | 45.5 | 2018-06-28 | 2 | 2.88 | 60 | 27.4 | 4 | False | ... | 7.4 | 0 | Кронштадт | 67763.0 | 49488.0 | 2 | 342.0 | 3 | 614.0 | 166 |
| 16731 | 9 | 3978000.0 | 40.0 | 2018-09-24 | 1 | 2.65 | 52 | 10.5 | 18 | False | ... | 14.0 | 0 | Санкт-Петербург | 20728.0 | 12978.0 | 1 | 793.0 | 0 | NaN | 45 |
2 rows × 22 columns
data.at[2253, 'floors_total'] = 6
data.at[16731, 'floors_total'] = 22
Проверяю. Теперь порядок.
data[data['floors_total'] > 37]
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | kitchen_area | balcony | locality_name | airports_nearest | cityCenters_nearest | parks_around3000 | parks_nearest | ponds_around3000 | ponds_nearest | days_exposition |
|---|
0 rows × 22 columns
Зданий выше 37 этажей в Санкт-Петербурге нет. Аномальные значения выше 37 удалены.
'last_price' - цена на момент снятия с публикации¶pd.options.display.float_format ='{:,.2f}'.format
data.last_price.describe()
count 23,565.00 mean 6,540,058.26 std 10,910,934.72 min 12,190.00 25% 3,400,000.00 50% 4,646,000.00 75% 6,790,000.00 max 763,000,000.00 Name: last_price, dtype: float64
Вижу, что среднее значение цены 6,5 млн.руб. И стандартное отклонение составляет 10,9 млн. Вижу также, что максимальное значение цены 763 млн.руб. слишком высоко и влияет на среднюю цену. В нашем случае это значение явная аномалия. Поэтому посмотрю, какая цена является 99 перцентилем, то есть какая максимальная цена у 99 процентов объектов, исключая те, которые дороже.
np.percentile(data.last_price, 99)
36000000.0
data[data['last_price'] > 36000000].value_counts().sum()
105
Цена выше 36 милионов y 105 объектoв, что составляет менее одного процента от общего их количества, поэтому ограничу исследование диапазоном цен от 0 до 36 млн.рублей.
Делаю срез данных по объектам, цена которых не превышает 36 млн.рублей.
data = data[data.last_price <= 36000000]
В ценах объектов недвижимости есть объекты с аномально высокой ценой с точки зрения настоящего исследования. Оставляю для дальнейшего исследования объекты стоимостью до 36 млн.руб. включительно, которые составляют 99 всего количества. Объекты с более высокой ценой отбросил как аномальные
'total_area' - общая площадь¶data.total_area.describe()
count 23,330.00 mean 58.65 std 29.41 min 12.00 25% 40.00 50% 51.70 75% 68.72 max 470.30 Name: total_area, dtype: float64
Похожая ситуация как и с ценами. Посмотрю 99 перцентиль по общей площади
np.percentile(data.total_area, 99)
170.0
data[data['total_area'] > 175].value_counts().sum()
65
99 процентов значений общей площади лежат в диапазоне до 175 квадратных метров. Оставшиеся 65 объектов имеют площадь более 175 квадратных метров. Удаляю их как аномальные
data = data[data['total_area'] <= 175]
'ceiling_height' - высота потолка¶Смотрю уникальные значения высоты потолков в столбце
data.ceiling_height.sort_values().unique()
array([ 1. , 1.2 , 1.75, 2. , 2.2 , 2.25, 2.3 , 2.34,
2.4 , 2.45, 2.46, 2.47, 2.48, 2.49, 2.5 , 2.51,
2.52, 2.53, 2.54, 2.55, 2.56, 2.57, 2.58, 2.59,
2.6 , 2.61, 2.62, 2.63, 2.64, 2.65, 2.66, 2.67,
2.68, 2.69, 2.7 , 2.71, 2.72, 2.73, 2.74, 2.75,
2.76, 2.77, 2.78, 2.79, 2.8 , 2.81, 2.82, 2.83,
2.84, 2.85, 2.86, 2.87, 2.88, 2.89, 2.9 , 2.91,
2.92, 2.93, 2.94, 2.95, 2.96, 2.97, 2.98, 2.99,
3. , 3.01, 3.02, 3.03, 3.04, 3.05, 3.06, 3.07,
3.08, 3.09, 3.1 , 3.11, 3.12, 3.13, 3.14, 3.15,
3.16, 3.17, 3.18, 3.2 , 3.21, 3.22, 3.23, 3.24,
3.25, 3.26, 3.27, 3.28, 3.29, 3.3 , 3.31, 3.32,
3.33, 3.34, 3.35, 3.36, 3.37, 3.38, 3.39, 3.4 ,
3.42, 3.44, 3.45, 3.46, 3.47, 3.48, 3.49, 3.5 ,
3.51, 3.52, 3.53, 3.54, 3.55, 3.56, 3.57, 3.58,
3.59, 3.6 , 3.62, 3.63, 3.65, 3.66, 3.67, 3.68,
3.7 , 3.75, 3.78, 3.8 , 3.83, 3.84, 3.85, 3.88,
3.9 , 3.93, 3.95, 3.98, 4. , 4.06, 4.1 , 4.14,
4.15, 4.19, 4.2 , 4.25, 4.3 , 4.37, 4.4 , 4.5 ,
4.7 , 4.8 , 5. , 5.3 , 5.8 , 8. , 8.3 , 10.3 ,
14. , 20. , 22.6 , 24. , 25. , 26. , 27. , 27.5 ,
32. , 100. ])
В Санкт-Петербурге есть исторические здания, в которых высота потолков превышает 4 метра, также могут быть и современные элитные квартиры и апартаменты с аналогичыми показателями. Количество таких объектов невелико, учитывавая среднее значение высоты потолка в 2.77 м. По действующим нормам минимальная высота потоков не должна быть ниже 2.5, однако, есть на рынке помещения, в которых данный стандарт не соблюден или они были построены до его принятия. Также в датасете есть апартаменты, которые могут и не выдерживать нормы для жилых помещений - например, расположенные в цокольных этажах. В данном случае принимаю решение оставить строки с высотой потолков от 2.2м до 5м. При этом есть двузначные значения, которые могут быть опечатками и нужно перенести десятичную точку на один знак, отфильтровать значения 2.2 м и ниже, 5 м и выше, обновить основной датасет:
height_more_five_change = data.query('ceiling_height >= 5').copy()
height_more_five_change['ceiling_height'] = height_more_five_change['ceiling_height'] * 0.1
data.loc[data['ceiling_height'] >= 5, 'ceiling_height'] = height_more_five_change['ceiling_height']
Срезаю объекты с потолками выше 5 метров
data = data[data['ceiling_height'] < 5]
Срезаю объекты с потолками ниже 2.5 метра
data = data[data['ceiling_height'] > 2.2]
В датасете обнаружены пропущенные значения. В тех столбцах, где количество пропусков незначительно, соответствующие строки удалены (название населенного пункта, количество этажей). Часть пропусков заполнены медианными значениями (жилая площадь,площадь кухни с корректировкой относительно общей, высота потолков), часть 0 (количество балконов) и значением False в столбце, определяющем является ли объект апартаментами. В столбцах с картографическими данными пропуски также можно было заполнить применяя один из методов, однако принял решение пока их оставить как есть ввиду низкой достоверности заполняемых значений.
Проверка на дубликаты явных дубликатов не выявила
Изучены типы данных, в отдельных столбцах приведены к необходимым для исследования типам 'int', 'datetime'
Выявлены аномальные значения в ряде столбцов и удалены
'price_per_meter'¶data['price_per_meter'] = data['last_price'] / data['total_area']
data['price_per_meter'] = data['price_per_meter'].round(2)
'first_exp_weekday'¶data['first_exp_weekday'] = data['first_day_exposition'].dt.weekday
'first_exp_month'¶data['first_exp_month'] = data['first_day_exposition'].dt.month
'first_exp_year'¶data['first_exp_year'] = data['first_day_exposition'].dt.year
'type_floor'¶change_a = (data['floor'] == 1).to_list()
change_b = (data['floor'] == data['floors_total']).to_list()
change_c = ((data['floor'] != data['floors_total']) & (data['floor'] != 1)).to_list()
condlist = [change_a, change_b, change_c]
choices = ['первый', 'последний', 'другой']
data['type_floor'] = np.select(condlist, choices)
'cityCenters_nearest_km'¶data['cityCenters_nearest_km'] = data['cityCenters_nearest'] / 1000
data['cityCenters_nearest_km'] = data['cityCenters_nearest_km'].round()
%config InlineBackend.figure_format = 'retina' # improves visualisation
'total_area'¶# histogram `total_area`
data.total_area.plot(kind='hist', bins=180, grid=True, figsize=(10,4), edgecolor='black',
title="Распределение значений общей площади",
xlabel="Общая площадь (кв.м)",
ylabel="Количество значений"
);
data.total_area.describe()
count 23,098.00 mean 57.18 std 24.42 min 12.00 25% 40.00 50% 51.20 75% 68.00 max 175.00 Name: total_area, dtype: float64
Вывод: Вижу по графику, что значения площадей распределились по группам. И основной объем приходится на квартиры до 40 квадратных метров и от 40 до 60 квадратны метров. Видимо это одно- и двухкомнатные квартиры. Среднее значение общей площади квартиры 57.2 кв.м, медианное значение - 51.2 кв.м
'living_area'¶# histogram `living_area`
data.living_area.plot(kind='hist', bins=140, grid=True, figsize=(10,4), edgecolor='black',
title="Распределение значений жилой площади",
xlabel="Жилая площадь (кв.м)",
ylabel="Количество значений"
);
data.living_area.describe()
count 23,098.00 mean 32.56 std 16.15 min 2.00 25% 18.20 50% 30.00 75% 42.00 max 140.00 Name: living_area, dtype: float64
Вывод: На диаграмме явно прослеживаются три пика. Это очевидно одно, двух и трехкомнатные квартиры. И наибольшее предложение в сегменте однокомнатных квартир с площадью комнаты 16-18 квадратных метров. Среднее значение жилой площади 32.56 кв.м, медианное 30 кв.м
'kitchen_area'¶# histogram `kitchen_area`
data.kitchen_area.plot(kind='hist', bins=100, grid=True, figsize=(10,4), edgecolor='black',
title="Распределение значений площади кухни",
xlabel="Площадь кухни (кв.м)",
ylabel="Количество значений"
);
data.kitchen_area.describe()
count 23,098.00 mean 10.04 std 4.84 min 0.00 25% 7.10 50% 9.00 75% 11.20 max 65.00 Name: kitchen_area, dtype: float64
Вывод: На диаграмме явно прослеживаются несколько групп квартир: с площадью кухни около 6-7 квадратных метров - это стандартные серии домов 60-х - 70-х годов прошлого века и большая часть ранее построенных зданий, в диапазоне 8-9 квадратных метров - это улучшенные серии 80-х-начала 90-х годов и на третьем месте кухни площадью 10 квадратных метров и более - это уже здания с середины 90-х годов по настоящее время. Распределение вполне адекватно отражает особенности существующего жилого фонда. Нулевые значения площадей кухни - в студиях и возможно апартаментах.
'last_price'¶Изменю масштаб значений цены на млн.руб. для удобства отображения.
data['last_price'] = data['last_price'] / 1000000
# histogram `last_price`
data.last_price.plot(kind='hist', bins=100, grid=True, figsize=(10,4), edgecolor='black',
title="Распределение значений цены объекта",
xlabel="Цена (млн.руб)",
ylabel="Количество значений"
);
data.last_price.describe()
count 23,098.00 mean 5.69 std 4.01 min 0.01 25% 3.40 50% 4.60 75% 6.60 max 36.00 Name: last_price, dtype: float64
Вывод: Большинство предлагаемых к продаже объектов собственники оценивают в диапазоне от 3 до 7 млн. рублей, средняя цена предложения 5.69 млн.руб., медианное значение цены - 4.6 млн.руб.
'rooms'¶# histogram `rooms`
data.rooms.plot(kind='hist', bins=20, grid=True, figsize=(6,4), edgecolor='black',
title="Распределение значений количества комнат",
xlabel="Количество комнат (шт.)",
ylabel="Количество значений"
);
Смотрю точное количество объектов с количеством комнат в абсолютных и относительных величинах
data.rooms.value_counts()
rooms 1 7986 2 7875 3 5688 4 1075 5 225 0 192 6 42 7 14 8 1 Name: count, dtype: int64
data.rooms.value_counts(normalize=True)
rooms 1 0.35 2 0.34 3 0.25 4 0.05 5 0.01 0 0.01 6 0.00 7 0.00 8 0.00 Name: proportion, dtype: float64
Продаваемые объекты имеют в основном одну - 35% или две комнаты - 34%, доля трехкомнатных - 25%. Таким образом, эти три типа планировок составляют 94% всех предложений о продажу. Есть нулевые значения комнат. Вполне возможно, они могли появиться при заполнении объявлений по студиям
'ceiling_height'¶# histogram `ceiling_height`
data.ceiling_height.plot(kind='hist', bins=50, grid=True, figsize=(6,4), edgecolor='black',
title="Распределение значений высоты потолков",
xlabel="Высота потолков (метры)",
ylabel="Количество значений"
);
data.ceiling_height.describe()
count 23,098.00 mean 2.69 std 0.20 min 2.25 25% 2.60 50% 2.65 75% 2.70 max 4.80 Name: ceiling_height, dtype: float64
Вывод: Распределение квартир по высоте потолков напоминают графики распределения квартир по общей площади и площади кухни - похожие два пика с большим количеством значений. Это закономерно, так как высота потолка, как и общая площадь, площадь кухни зависят от проекта дома и года постройки. Исторически в Санкт-Петербурге существуют районы массовой комплексной застройки, где находятся здания типовых серий 60-х - 70-х годов прошлого века со стандартными потолками 2.5м. Более старые здания попали в другой диапазон: здания до 60-х годов, как и здания после 70-х имеют более высокие потолки от 2.6 до 3 метров
'floor'¶# histogram `floor`
data.floor.plot(kind='hist', bins=74, grid=True, figsize=(8,4), edgecolor='black',
title="Распределение квартир по этажам",
xlabel="Этаж (номер)",
ylabel="Количество значений"
);
data.floor.value_counts()
floor 2 3282 3 2990 1 2877 4 2709 5 2542 6 1262 7 1177 8 1055 9 1037 10 677 11 515 12 512 13 374 15 336 14 329 16 308 17 222 18 173 19 141 21 119 22 111 20 108 23 98 24 60 25 44 26 24 27 10 28 1 30 1 29 1 32 1 33 1 31 1 Name: count, dtype: int64
Вывод: Распределение по этажам также повторяет вышеописанные наблюдения о составе жилого фонда. Очевидно, что большая часть предложений о продаже в пятиэтажных зданиях. Больше всего (в сравнении с каждой другой категорией отдельно) предложений на вторых этажах.
'type_floor'¶data.type_floor.value_counts()
type_floor другой 17012 последний 3209 первый 2877 Name: count, dtype: int64
data.type_floor.value_counts(normalize=True)
type_floor другой 0.74 последний 0.14 первый 0.12 Name: proportion, dtype: float64
Вывод: На последних этажах продается 14%, а на первых 12% от всех объектов, а вместе они составляют 26 процентов от всего объема предложения
'floors_total'¶# histogram `floors_total`
data.floors_total.plot(kind='hist', bins=30, grid=True, figsize=(6,4), edgecolor='black',
title="Распределение значений этажности зданий",
xlabel="Количество этажей (шт)",
ylabel="Количество значений"
);
Вывод: Диаграмма подтверждает выше сделанные наблюдения: самая большая группа предложений в пятиэтажных домах, второе место - девятиэтажные. Это в основной массе здания типовой жилой застройки 60-х - 80-х годов прошлого века
'cityCenters_nearest' Санкт-Петербурга¶data['cityCenters_nearest'] = data['cityCenters_nearest'] / 1000
# histogram `cityCenters_nearest`
data.cityCenters_nearest.plot(kind='hist', bins=150, grid=True, figsize=(10,4), edgecolor='grey',
title="Распределение значений расстояний до центра города",
xlabel="Расстояние до центра города (км)",
ylabel="Количество значений"
);
data.cityCenters_nearest.describe()
count 17,622.00 mean 14.37 std 8.58 min 0.18 25% 9.59 50% 13.22 75% 16.38 max 65.97 Name: cityCenters_nearest, dtype: float64
Вывод: Большая часть объектов из выборки расположены на расстоянии примерно от 7 до 17 километров от центра города. Среднее расстояние 14.37км, медианное значение расстояния 13.22км
'airports_nearest'¶data['airports_nearest'] = data['airports_nearest'] / 1000
# histogram `airports_nearest`
data.airports_nearest.plot(kind='hist', bins=150, grid=True, figsize=(10,4), edgecolor='grey',
title='Распределение значений расстояний до аэропорта "Пулково"',
xlabel="Расстояние до аэропорта (км)",
ylabel="Количество значений"
);
data.airports_nearest.describe()
count 17,602.00 mean 28.84 std 12.73 min 0.00 25% 18.45 50% 26.88 75% 37.41 max 84.87 Name: airports_nearest, dtype: float64
Вывод: Большая часть объектов из выборки расположены на расстоянии от 10 до 40 километров от аэропорта "Пулково". Среднее расстояние 28.84км, медианное значение расстояния 26.88км
'parks_nearest'¶# histogram `parks_nearest`
data.parks_nearest.plot(kind='hist', bins=150, grid=True, figsize=(10,4), edgecolor='grey',
title='Распределение значений расстояний до ближайшего парка',
xlabel="Расстояние до ближайшего парка (м)",
ylabel="Количество значений"
);
Вывод: Подавляющее количество квартир расположено на расстоянии не более одного километра от ближайшего парка. Санкт-Петербург - зеленый город.
'ponds_nearest'¶# histogram `ponds_nearest`
data.ponds_nearest.plot(kind='hist', bins=150, grid=True, figsize=(10,4), edgecolor='grey',
title='Распределение значений расстояний до ближайшего водоема',
xlabel="Расстояние до ближайшего водоема (м)",
ylabel="Количество значений"
);
Вывод: Аналогично паркам водоемы расположены на расстоянии не более одного километра от большинства квартир.
'first_exp_weekday'¶# histogram `first_exp_weekday`
fig, ax = plt.subplots()
ax.hist(data['first_exp_weekday'], bins = 25, rwidth = 0.6)
ax.set_title('День недели первой публикации объявления, 0 - понедельник')
ax.set_ylabel('Количество объявлений')
plt.show()
Вывод: Чаще объявления публиковались во вторник и четверг. Реже всего в воскресенье.
'first_exp_month'¶# # histogram `first_exp_month`
fig, ax = plt.subplots()
ax.hist(data['first_exp_month'], bins = 24, rwidth = 0.5)
ax.set_title('Месяц первой публикации объявления')
ax.set_ylabel('Количество объявлений')
plt.show()
Вывод: Самые активные по публикациям февраль, март, апрель и ноябрь. Самый "ленивый" для публикаций месяц май, за ним январь. Видимо из-за длинных выходных.
days_exposition).¶# histogram `days_exposition`
data.days_exposition.plot(kind='hist', bins=100, grid=True, figsize=(10,4), edgecolor='black',
title="Распределение объектов по количеству дней в продаже",
ylabel="Количество объявлений",
xlabel="Количество дней в продаже"
);
data.days_exposition.describe()
count 20,035.00 mean 178.79 std 217.71 min 1.00 25% 44.00 50% 94.00 75% 228.00 max 1,580.00 Name: days_exposition, dtype: Float64
Вывод: Среднее значение по столбцу 177 дней. При этом половина всех квартира проданы за 94 дня и быстрее. Еще четверть квартир продавались от 94 до 228 дней. Быстрой можно считать продажу быстрее, чем за 44 дня. Слишком долгая экспозиция объекта - более 225 дней
Посмотрю дополнително как распределены значения внутри диапазона от 0 до 100 дней
# histogram `days_exposition` от 0 до 100 дней
data['days_exposition'][data['days_exposition'] < 100].plot(kind='hist', bins=100,
grid=True, figsize=(10,4), edgecolor='black',
title="Распределение объектов по количеству дней в продаже",
ylabel="Количество объявлений",
xlabel="Количество дней в продаже"
);
data['days_exposition'][data['days_exposition'] < 100].describe()
count 10,333.00 mean 44.62 std 26.69 min 1.00 25% 21.00 50% 45.00 75% 63.00 max 99.00 Name: days_exposition, dtype: Float64
Вывод: Есть пики на 45-м дне и на 60-м. Предполагаю, что это связано с автоматической настройкой платформы Яндекс.Недвижимость, где размещаются объявления. Видимо при определенных условиях, они автоматически публикуются или автоматически снимаются с рекламы.
Изучy, зависит ли цена от:
Построю графики, которые покажут зависимость цены от указанных выше параметров
# scatter plot 'last_price' and 'total_area'
x=data.last_price
y=data.total_area
plt.figure(figsize=(9, 5))
plt.scatter(x, y, s=20, alpha=0.3)
slope, intercept = np.polyfit(x, y, 1)
plt.grid()
plt.title("Зависимость между ценой и общей площадью")
plt.xlabel("Цена в млн.руб.")
plt.ylabel("Площадь в кв.м.")
plt.plot(x, slope*x + intercept, color='red', linestyle='dotted', label='Линия тренда')
plt.legend()
plt.show()
Считаю коэффициент корреляции между значениями общей площади и цены
data['last_price'].corr(data['total_area'])
0.771500307915153
Коэффициент положительный и его значение 0.77 достаточно близко к единице, что отражает сильную положительную взаимосвязь между ценой и общей площадью. При увеличении одного показателя увеличивается и другой
# scatter plot 'last_price' and 'living_area'
x=data.last_price
y=data.living_area
plt.figure(figsize=(9, 5))
plt.scatter(x, y, s=20, alpha=0.3)
slope, intercept = np.polyfit(x, y, 1)
plt.grid()
plt.title("Зависимость между ценой и жилой площадью")
plt.xlabel("Цена в млн.руб.")
plt.ylabel("Площадь в кв.м.")
plt.plot(x, slope*x + intercept, color='red', linestyle='dotted', label='Линия тренда')
plt.legend()
plt.show()
Считаю коэффициент корреляции между значениями жилой площади и цены
data['last_price'].corr(data['living_area'])
0.6319549666825636
Коэффициент положительный и его значение 0.63 говорит о наличии взаимосвязи между ценой и общей площадью. При увеличении одного показателя увеличивается и другой. Однако, в данном случае эта зависимость ниже, чем между ценой и общей площадью.
# scatter plot 'last_price' and 'kitchen_area'
x=data.last_price
y=data.kitchen_area
plt.figure(figsize=(9, 5))
plt.scatter(x, y, s=20, alpha=0.3)
slope, intercept = np.polyfit(x, y, 1)
plt.grid()
plt.title("Зависимость между ценой и площадью кухни")
plt.xlabel("Цена в млн.руб.")
plt.ylabel("Площадь в кв.м.")
plt.plot(x, slope*x + intercept, color='red', linestyle='dotted', label='Линия тренда')
plt.legend()
plt.show()
Считаю коэффициент корреляции между значениями площади кухни и цены
data['last_price'].corr(data['kitchen_area'])
0.5644287348572664
Картина та же, что и выше, но зависимость несколько меньше.
data.groupby(['rooms'])['last_price'].agg('mean').round(2).plot(
xlabel='количество комнат', ylabel='средняя цена', kind='bar')
plt.show()
data['last_price'].corr(data['rooms'])
0.47360555025077206
Коэффициент корреляции положительный, чем больше комнат, тем выше цена
При сравнении цен по типам этажей, годам, месяцам и дням размешения фактически сравниваю соответствующие группы объектов. Так как объекты группируются по категориям, то к цене применяю агрегирующую функцию - средняя цена (mean)
data.groupby(['type_floor'])['last_price'].agg('mean').round(2).plot(
xlabel='тип этажа', rot=45, ylabel='средняя цена', kind='bar' )
plt.show()
В среднем на первом этаже квартиры оцениваются дешевле, затем следуют последние этажи и лучше цены на квартиры, расположенные на других этажах
Чтобы посчитать корреляцию, привожу типы столбцов 'первый', 'последний', 'другой' к числовым значениям: 1, 2, 3
change_a = (data['type_floor'] == 'первый')
change_b = (data['type_floor'] == 'последний')
change_c = (data['type_floor'] == 'другой')
conditions = [change_a, change_b, change_c]
choices = [1, 2, 3]
data['type_floor_num'] = np.select(conditions, choices)
data['last_price'].corr(data['type_floor_num'])
0.12227561700714402
Коэффициент корреляции положительный, но не значительно выше 0, взаимосвязь есть, но невысокая
data.groupby(['first_exp_year'])['last_price'].agg('mean').round(2).plot(
xlabel='год публикации', ylabel='средняя цена', kind='bar'
)
plt.title('Год первой публикации объявления')
plt.show();
Неожиданный результат. Оказывается средняя цена предложений снижалась несколько лет, а затем снова начался рост. Это отличается от общего мнения, что цены на недвижимость всегда растут
data.groupby(['first_exp_month'])['last_price'].agg('mean').round(2).plot(
xlabel='месяц публикации', ylabel='средняя цена', kind='bar')
plt.title('Месяц первой публикации объявления, 1 - январь')
plt.show();
Самые высокие в среднем цены предложений в апреле и сентябре, самые низкие в июне
data.groupby(['first_exp_weekday'])['last_price'].agg('mean').round(2).plot(
xlabel='день публикации', ylabel='средняя цена', kind='bar')
plt.title('День недели первой публикации объявления, 0 - понедельник')
plt.show();
На неделе средняя цена предложения выше во вторник, самые низкие в субботу
Самое большое влияние на цену квартир оказывают количество комнат, а также параметры площади: общей, жилой, кухни. Из объективных факторов следует отметить год публикации объявления. Очевидно, что рыночная ситуация меняется от года к году и влияет на цены квартир и платежеспособный спрос
Ищу 10 населенных пунктов с наибольшим числом объявлений
data['locality_name'].value_counts().head(10)
locality_name Санкт-Петербург 15221 посёлок Мурино 520 посёлок Шушары 439 Всеволожск 397 Пушкин 363 Колпино 337 посёлок Парголово 326 Гатчина 307 деревня Кудрово 299 Выборг 233 Name: count, dtype: int64
top_list = data['locality_name'].value_counts().head(10).index.to_list()
data_top_ten = data.loc[data['locality_name'].isin(top_list)].copy()
Вывожу стобчатую диаграмму для сравнения средних цен за квадратный метр в ТОП-10 населенных пунктах по количеству объявленй о продаже. Группирую данные по названиям населенных пунктов, агрегирующую функцию применяю - среднее значение
data_top_ten.groupby('locality_name')['price_per_meter'].agg('mean'
).round(2).sort_values().plot(
xlabel='Населенный пункт', ylabel='Среднее значение цены (руб/кв.м)', kind='bar', rot=45)
plt.title('Среднее значение цены кв.м в ТОП-10 населенных пунктах по количеству объявлений')
plt.show();
data_top_ten.groupby('locality_name')['price_per_meter'].agg('mean'
).round(2).sort_values(ascending=False)
locality_name Санкт-Петербург 111,204.75 Пушкин 103,047.05 деревня Кудрово 92,473.55 посёлок Парголово 90,332.26 посёлок Мурино 85,673.26 посёлок Шушары 78,551.34 Колпино 75,333.30 Гатчина 68,746.15 Всеволожск 68,719.32 Выборг 58,172.39 Name: price_per_meter, dtype: float64
Населенные пункты с самой высокой стоимостью квадратного метра из ТОП-10 по количеству объявлений
print(data_top_ten[data_top_ten['price_per_meter'] ==
data_top_ten['price_per_meter'].max()]['locality_name'].unique())
['Санкт-Петербург']
data_top_ten[data_top_ten['price_per_meter'] == data_top_ten['price_per_meter'].max()]
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | ponds_around3000 | ponds_nearest | days_exposition | price_per_meter | first_exp_weekday | first_exp_month | first_exp_year | type_floor | cityCenters_nearest_km | type_floor_num | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4859 | 16 | 28.00 | 33.00 | 2019-04-29 | 1 | 3.50 | 5 | 17.60 | 2 | False | ... | 3 | 119.00 | <NA> | 848,484.85 | 0 | 4 | 2019 | другой | 1.00 | 3 |
| 17172 | 14 | 28.00 | 33.00 | 2019-04-30 | 1 | 3.50 | 5 | 17.60 | 2 | False | ... | 3 | 27.00 | <NA> | 848,484.85 | 1 | 4 | 2019 | другой | 1.00 | 3 |
2 rows × 29 columns
Населенные пункты с самой низкой стоимостью квадратного метра из ТОП-10 по количеству объявлений
print(data_top_ten[data_top_ten['price_per_meter'] ==
data_top_ten['price_per_meter'].min()]['locality_name'].unique())
['Санкт-Петербург']
min_spb = data_top_ten.query('locality_name == "Санкт-Петербург"')['price_per_meter'].min()
min_spb
111.83
Минимальное значение цены за квадратный метр выглядит в Санкт-Петербурге очень странно, похоже ошибка в данных. Удаляю эту строку как аномальную и повторяю поиск минимального значения
data_top_ten = data_top_ten.loc[data_top_ten['price_per_meter'] != min_spb]
data_top_ten[data_top_ten['price_per_meter'] == data_top_ten['price_per_meter'].min()]
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | ponds_around3000 | ponds_nearest | days_exposition | price_per_meter | first_exp_weekday | first_exp_month | first_exp_year | type_floor | cityCenters_nearest_km | type_floor_num | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23477 | 3 | 1.45 | 138.00 | 2018-07-06 | 3 | 2.65 | 2 | 58.00 | 2 | False | ... | <NA> | NaN | 52 | 10,507.25 | 4 | 7 | 2018 | последний | NaN | 2 |
1 rows × 29 columns
data_top_ten[data_top_ten['price_per_meter'] == data_top_ten['price_per_meter'].min()]['locality_name']
23477 Гатчина Name: locality_name, dtype: object
Вывод: Самая высокая цена за квадратный метр среди 10-ти городов с максимальным количеством квартир в продаже 848484.85 рублей за квадратный метр в Санкт-Петербурге, самая низкая в Свири - 7962.96 рубля. Максимальная средняя цена продажи в объявлениях в этих городах составляет 111,204.75 рублей в городе Санкт-Петербург
data_spb = data[data['locality_name'] == 'Санкт-Петербург'].copy()
Ранее я округлил до целых и привел к типу int данные в столбце расстояние до центра в километрах, поэтому группирую данные по расстоянию до центра города в сводную таблицу, выведу ТОП-5 километров от центра по максимальной средней цене за кв.м и ТОП-5 километров по минимальной цене за кв.м, рассчитаю коэффициент корреляции и построю график, который отражает зависимости
Проверяю пропуски, так как для исследования корреляции мне нужны стобцы с полностью заполненными данными c столбце cityCenters_nearest_km
data_spb.cityCenters_nearest_km.isna().sum()
52
Учитывая незначительное количество пропущенных данных, просто отброшу строки с пропусками
data_spb = data_spb.dropna(subset=['cityCenters_nearest_km'])
pd.pivot_table(data_spb, index='cityCenters_nearest_km',
values=('last_price', 'price_per_meter'), aggfunc=np.mean)
| last_price | price_per_meter | |
|---|---|---|
| cityCenters_nearest_km | ||
| 0.00 | 16.73 | 167,305.31 |
| 1.00 | 11.71 | 146,048.97 |
| 2.00 | 10.69 | 129,310.18 |
| 3.00 | 9.57 | 118,102.69 |
| 4.00 | 10.28 | 126,919.16 |
| 5.00 | 10.58 | 133,450.95 |
| 6.00 | 9.99 | 135,883.85 |
| 7.00 | 9.97 | 136,204.61 |
| 8.00 | 8.71 | 123,439.92 |
| 9.00 | 6.77 | 112,691.45 |
| 10.00 | 6.32 | 112,660.26 |
| 11.00 | 6.09 | 108,136.48 |
| 12.00 | 5.77 | 107,558.54 |
| 13.00 | 6.03 | 108,157.21 |
| 14.00 | 5.57 | 104,176.53 |
| 15.00 | 5.76 | 104,201.23 |
| 16.00 | 5.32 | 100,482.24 |
| 17.00 | 5.19 | 96,997.26 |
| 18.00 | 4.81 | 96,389.65 |
| 19.00 | 5.05 | 98,658.39 |
| 20.00 | 5.99 | 103,056.54 |
| 21.00 | 5.49 | 94,469.58 |
| 22.00 | 5.31 | 91,405.54 |
| 23.00 | 4.69 | 92,063.68 |
| 24.00 | 3.85 | 85,736.90 |
| 25.00 | 4.05 | 91,531.37 |
| 26.00 | 4.01 | 87,798.87 |
| 27.00 | 8.30 | 132,115.71 |
| 28.00 | 5.03 | 81,161.91 |
| 29.00 | 4.24 | 72,953.37 |
ТОП-5 километров от центра города с максимальными средними ценами за кв.м
pd.pivot_table(data_spb, index='cityCenters_nearest_km',
values=('last_price', 'price_per_meter'),
aggfunc=np.mean).sort_values(by='price_per_meter', ascending=False).head(5)
| last_price | price_per_meter | |
|---|---|---|
| cityCenters_nearest_km | ||
| 0.00 | 16.73 | 167,305.31 |
| 1.00 | 11.71 | 146,048.97 |
| 7.00 | 9.97 | 136,204.61 |
| 6.00 | 9.99 | 135,883.85 |
| 5.00 | 10.58 | 133,450.95 |
ТОП-5 километров от центра города с минимальными средними ценами за кв.м
pd.pivot_table(data_spb, index='cityCenters_nearest_km',
values=('last_price', 'price_per_meter'),
aggfunc=np.mean).sort_values(by='price_per_meter').head(5)
| last_price | price_per_meter | |
|---|---|---|
| cityCenters_nearest_km | ||
| 29.00 | 4.24 | 72,953.37 |
| 28.00 | 5.03 | 81,161.91 |
| 24.00 | 3.85 | 85,736.90 |
| 26.00 | 4.01 | 87,798.87 |
| 22.00 | 5.31 | 91,405.54 |
data_spb[['cityCenters_nearest_km', 'last_price', 'price_per_meter']].corr()
| cityCenters_nearest_km | last_price | price_per_meter | |
|---|---|---|---|
| cityCenters_nearest_km | 1.00 | -0.41 | -0.34 |
| last_price | -0.41 | 1.00 | 0.65 |
| price_per_meter | -0.34 | 0.65 | 1.00 |
# scatter plot 'last_price' and 'cityCenters_nearest_km' spb
y=data_spb.last_price
x=data_spb.cityCenters_nearest_km
plt.figure(figsize=(9, 5))
plt.scatter(x, y, s=20, alpha=0.3)
plt.grid()
plt.title("Зависимость между ценой объекта и расстоянием до центра города")
plt.xlabel("Расстояние до центра города в км.")
plt.ylabel("Цена в млн.руб")
slope, intercept = np.polyfit(x, y, 1)
plt.plot(x, slope*x + intercept, color='red', linestyle='dotted', label='Линия тренда')
plt.legend()
plt.show()
На графике присутствуют объекты, расположенные далее 20-м км от центра с ценой выше 10 млн. рублей. Особенно выделяются 22-й и 27-й км. Посмотрю их ближе, для этого сделаю срез
data_spb.query('(cityCenters_nearest_km == 22 | cityCenters_nearest_km == 27) & last_price > 10')
| total_images | last_price | total_area | first_day_exposition | rooms | ceiling_height | floors_total | living_area | floor | is_apartment | ... | ponds_around3000 | ponds_nearest | days_exposition | price_per_meter | first_exp_weekday | first_exp_month | first_exp_year | type_floor | cityCenters_nearest_km | type_floor_num | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 748 | 13 | 14.35 | 74.00 | 2017-11-28 | 2 | 3.13 | 5 | 30.00 | 3 | False | ... | 0 | NaN | 128 | 193,918.92 | 1 | 11 | 2017 | другой | 27.00 | 3 |
| 15985 | 13 | 15.30 | 136.00 | 2018-03-20 | 4 | 2.65 | 16 | 86.00 | 7 | False | ... | 1 | 458.00 | 110 | 112,500.00 | 1 | 3 | 2018 | другой | 22.00 | 3 |
| 17519 | 6 | 16.47 | 133.40 | 2018-08-23 | 3 | 3.00 | 4 | 45.00 | 4 | False | ... | 2 | 251.00 | <NA> | 123,476.00 | 3 | 8 | 2018 | последний | 22.00 | 2 |
| 21042 | 0 | 12.00 | 60.00 | 2018-07-07 | 2 | 2.65 | 26 | 30.50 | 25 | False | ... | 1 | 400.00 | 90 | 200,000.00 | 5 | 7 | 2018 | другой | 22.00 | 3 |
| 21712 | 19 | 10.20 | 99.30 | 2019-02-11 | 2 | 2.65 | 17 | 30.00 | 16 | False | ... | 1 | 1,052.00 | 61 | 102,719.03 | 0 | 2 | 2019 | другой | 22.00 | 3 |
5 rows × 29 columns
Вывод Очевидно, что корелляция между расстоянием от центра Санкт-Петербурга и ценой отрицательная, то есть чем больше расстояние от центра города, тем ниже цена. Однако на расстоянии от 22 до 27 километра присутствуют необычно дорогие объекты в общем количестве 5 штук. При ближайшем рассмотрении вижу, что это крупногабаритные квартиры с высокими потолками видимо в домах современной постройки. Могу предположить, что это могут быть квартиры повышенного качества, бизнесс класс или элитное жилье в курортных районах Санкт-Петербурга, например в локациях Ольгино, Лисий Нос. Учитывая их незначительное количество, такие квартиры не оказывают серьезного влияния на общую тенденцию
В процессе исследования были обнаружены и по возможности устранены аномалии в следующих столбцах: этажность зданий, высота потолков, площадь общая, кухни и жилая. Также отсечены слишком дорогие квартиры, стоимость которых могла влиять на результат.
В датасете было обнаружено большое количество картографических данных, при этом пропуски по нескольким столбцам в ощутимом по размерам объеме, особенно по Ленинградской области. Полагаю, это связано с какими-то техническими вопросами, потому что пользователи обычно совершают разные ошибки, а в этом случае прослеживается одна и таже закономерность на большом объеме. Отсутствие данных возможно из-за настроек автоматического заполнения расстояний по геопозиции. Принято решение эти пропуски не заполнять.
Исследованы зависимости различных параметров друг от друга: цена от расстояния до центра, цена от площадей общей, жилой, кухни; цена от количества комнат. Также исследованы особенности рынка жилой недвижимости Санкт-Петербурга.
Выводы: